package com.kasuroid.core;

import java.util.Stack;

import com.kasuroid.KasuroidActivity;

import android.app.Activity;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Point;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.SurfaceHolder;

/**
 * Base core class of the kasuroid.
 * 
 * @author Tomasz Rusak
 * 
 */
public class Core
{
    private static final String TAG = "Core";

    private class CoreThread extends Thread
    {
        /** Indicate whether the thread is running or not. */
        private boolean mRunning = false;

        private Core mCore = null;

        private int mSleepTime = 20;

        public CoreThread(Core core)
        {
            // core should never be null.
            this.mCore = core;
        }

        public int runThread()
        {
            setRunning(true);
            start();

            return RetCode.SUCCESS;
        }

        public int stopThread()
        {
            // Shut down the core thread.
            boolean retry = true;
            setRunning(false);
            while (retry)
            {
                try
                {
                    this.join();
                    retry = false;
                }
                catch (InterruptedException e)
                {
                    Debug.err(getClass().getName(), e.toString());
                    return RetCode.FAILURE;
                }
            }

            return RetCode.SUCCESS;
        }

        @Override
        public void run()
        {
            while (mRunning)
            {
                // Debug.inf(getClass().getName(), "run()");

                mCore.runSlice();

                // Call sleep to be able to get some inputs from the user.
                try
                {
                    sleep(mSleepTime);
                }
                catch (InterruptedException e)
                {
                    e.printStackTrace();
                }
            }
        }

        /**
         * Used to signal the thread whether it should be running or not.
         * Passing true allows the thread to run; passing false will shut it
         * down if it's already running. Calling start() after this was most
         * recently called with false will result in an immediate shutdown.
         * 
         * @param b
         *            true to run, false to shut down
         */
        public void setRunning(boolean run)
        {
            mRunning = run;
        }

        /**
         * Return the current thread running status. When tread beeing
         * terminated it may return FALSE, but thread is still running (code is
         * in the waiting state..).
         * 
         * @return Thread running status.
         */
        public boolean isRunning()
        {
            return mRunning;
        }

        public void setSleepTime(int sleepTime)
        {
            mSleepTime = sleepTime;
        }
    }

    private int mState = StateCode.TERMINATED;

    private TimerManager mTimerManager = null;

    private SoundManager mSoundManager = null;

    private SceneManager mSceneManager = null;

    private Renderer mRenderer = null;

    private TextureManager mTextureManager = null;

    /** Context in which the core is running. */
    private Context mContext = null;

    private SurfaceHolder mSurface = null;

    /** Indicates if screen/canvas is ready for drawing. */
    private boolean mScreenExists = false;

    private CoreThread mCoreThread = null;

    private static Core mInstance = null;

    private static final Object mLock = new Object();

    private Stack<GameState> mGameStatesStack = null;

    private Timer mTimer;

    private Point mFpsPosition;

    private boolean mFpsVisible;

    private boolean mFirstStart = true;

    /**
     * Core is a singleton.
     * 
     * @return Instance of the Core.
     */
    static synchronized public Core getInstance()
    {
        if (mInstance == null)
            mInstance = new Core();

        return mInstance;
    }

    private Core()
    {
        Debug.inf(getClass().getName(), "Core()");
        mTimer = new Timer();
        mGameStatesStack = new Stack<GameState>();
        mFpsVisible = false;
        mFpsPosition = new Point(0, 0);
    }

    /**
     * Initializes the core.
     */
    public int init(SurfaceHolder surfaceHolder, Context context)
    {
        int ret = RetCode.SUCCESS;

        Debug.inf(getClass().getName(), "init()");

        if ((mState == StateCode.INITIALIZED) || (mState == StateCode.RUNNING)
                || (mState == StateCode.STOPPED))
        {
            Debug.warn(getClass().getName(),
                    "Core already initialized, state: " + mState);
            return ret;
        }

        // We are here (STATE_TERMINATED), so go and initialize everything.

        if ((surfaceHolder == null) || (context == null))
        {
            Debug.err(getClass().getName(), "Bad parameters!");
            return RetCode.BAD_PARAM;
        }

        mSurface = surfaceHolder;
        mContext = context;

        // mCoreThread = new CoreThread(this);

        // Initialize all the modules.
        mTimerManager = new TimerManager();
        ret = mTimerManager.init();
        if (ret != RetCode.SUCCESS)
        {
            Debug.err("Core", "Problem with initializing the TimerManager");
            return ret;
        }

        // TODO Create the main timer for the core.

        mSoundManager = new SoundManager();
        ret = mSoundManager.init();
        if (ret != RetCode.SUCCESS)
        {
            Debug.err("Core", "Problem with initializing the SoundManager");
            return ret;
        }

        mSceneManager = new SceneManager();
        ret = mSceneManager.init();
        if (ret != RetCode.SUCCESS)
        {
            Debug.err("Core", "Problem with initializing the SceneManager");
            return ret;
        }

        mTextureManager = new TextureManager();
        ret = mTextureManager.init();
        if (ret != RetCode.SUCCESS)
        {
            Debug.err("Core", "Problem with initializing the TextureManager");
            return ret;
        }

        mRenderer = new Renderer();
        ret = mRenderer.init();
        if (ret != RetCode.SUCCESS)
        {
            Debug.err("Core", "Problem with initializing the Renderer");
            return ret;
        }

        Fps.reset();

        mRenderer.setSurfaceHolder(mSurface);

        mState = StateCode.INITIALIZED;

        mFirstStart = true;

        return ret;
    }

    /**
     * Terminates the core.
     */
    public int term()
    {
        int ret = RetCode.SUCCESS;

        Debug.inf(getClass().getName(), "term()");

        if (mState == StateCode.TERMINATED)
        {
            Debug.warn(getClass().getName(), "Core already terminated");
            // Return SUCCESS, as it doesn't have any negative impact.
            return ret;
        }

        if (mState == StateCode.RUNNING)
        {
            Debug.warn(getClass().getName(),
                    "Core is still running, stopping..");
            ret = stop();
            if (ret != RetCode.SUCCESS)
            {
                Debug.err(getClass().getName(),
                        "Problem with stopping the core!");
                return ret;
            }
        }

        // We are in the proper state here so go and terminate everything.

        // Remove all active game states.
        ret = removeGameStates();
        if (ret != RetCode.SUCCESS)
        {
            Debug.err(getClass().getName(),
                    "Problem with removing the game states!");
        }
        mGameStatesStack.clear();

        // Don't really care about the return codes while terminating.
        // Just log error.
        ret = mTimerManager.term();
        if (ret != RetCode.SUCCESS)
        {
            Debug.err("Core", "Problem with terminating the TimerManager");
        }

        ret = mSoundManager.term();
        if (ret != RetCode.SUCCESS)
        {
            Debug.err("Core", "Problem with terminating the SoundManager");
        }

        ret = mSceneManager.term();
        if (ret != RetCode.SUCCESS)
        {
            Debug.err("Core", "Problem with terminating the SceneManager");
        }

        ret = mTextureManager.term();
        if (ret != RetCode.SUCCESS)
        {
            Debug.err("Core", "Problem with terminating the TextureManager");
        }

        ret = mRenderer.term();
        if (ret != RetCode.SUCCESS)
        {
            Debug.err("Core", "Problem with terminating the Renderer");
        }

        mState = StateCode.TERMINATED;

        mFirstStart = true;

        return ret;
    }

    public int start()
    {
        Debug.inf(getClass().getName(), "start()");

        if (mState == StateCode.TERMINATED)
        {
            Debug.err(getClass().getName(), "Wrong core state: " + mState);
            return RetCode.FAILURE;
        }

        if (mState == StateCode.RUNNING)
        {
            Debug.warn(getClass().getName(), "Core already running.");
            return RetCode.SUCCESS;
        }

        mCoreThread = new CoreThread(this);

        int ret = mCoreThread.runThread();
        if (ret != RetCode.SUCCESS)
        {
            Debug.err(getClass().getName(),
                    "Problem with running the core thread!");
            return ret;
        }

        mState = StateCode.RUNNING;

        mTimer.start();

        if (mFirstStart)
        {
            Debug.inf(TAG, "Calling activity ready()");
            ((KasuroidActivity) mContext).ready();
        }

        return RetCode.SUCCESS;
    }

    public int stop()
    {
        Debug.inf(getClass().getName(), "stop()");

        if ((mState == StateCode.INITIALIZED) || 
            (mState == StateCode.TERMINATED))
        {
            Debug.err(getClass().getName(), "Wrong core state: " + mState);
            return RetCode.FAILURE;
        }

        if (mState == StateCode.STOPPED)
        {
            Debug.warn(getClass().getName(), "Core already stopped.");
            return RetCode.SUCCESS;
        }

        int ret = mCoreThread.stopThread();
        if (ret != RetCode.SUCCESS)
        {
            Debug.err(getClass().getName(),
                    "Problem with stopping the core thread!");
            return ret;
        }

        mCoreThread = null;

        mState = StateCode.STOPPED;

        mTimer.stop();

        return RetCode.SUCCESS;
    }

    public int pause()
    {
        Debug.inf(getClass().getName(), "pause()");

        int ret = RetCode.SUCCESS;
        if (mGameStatesStack.isEmpty() == false)
        {
            // Pause the current state.
            ret = mGameStatesStack.peek().pause();
            if (ret != RetCode.SUCCESS)
            {
                Debug.err(getClass().getName(),
                        "Problem with pausing the game state!");
                return ret;
            }
        }

        return RetCode.SUCCESS;
    }

    public int resume()
    {
        Debug.inf(getClass().getName(), "resume()");

        int ret = RetCode.SUCCESS;
        if (mGameStatesStack.isEmpty() == false)
        {
            // Pause the current state.
            ret = mGameStatesStack.peek().resume();
            if (ret != RetCode.SUCCESS)
            {
                Debug.err(getClass().getName(),
                        "Problem with pausing the game state!");
                return ret;
            }
        }

        return RetCode.SUCCESS;
    }

    private long mAccumulator = 0;
    private final long TIME_STEP = 40;
    private final long MAX_ACCUMULATED_TIME = 1000;

    /**
     * Main loop of the core.
     */
    protected int runSlice()
    {
        int ret = RetCode.SUCCESS;

        synchronized (mLock)
        {
            if (mScreenExists)
            {
                try
                {
                    mRenderer.lock();

                    mTimer.update();
                    mAccumulator += mTimer.getTimeDeltaMillis();

                    // Make sure that we don't update the game too
                    // many times.
                    if (mAccumulator > MAX_ACCUMULATED_TIME)
                        mAccumulator = MAX_ACCUMULATED_TIME;
                    else if (mAccumulator < 0)
                        mAccumulator = 0;

                    GameState currentState = null;
                    if (mGameStatesStack.isEmpty() == false)
                    {
                        currentState = mGameStatesStack.peek();

                        while (mAccumulator > TIME_STEP)
                        {
                            // Update all active timers.
                            ret = mTimerManager.updateAll();
                            if (ret != RetCode.SUCCESS)
                            {
                                Debug.err(getClass().getName(),
                                        "Problem with updating timers!");
                                mRenderer.unlock();
                                return ret;
                            }

                            // Update the current state logic.
                            ret = currentState.update();
                            if (ret != RetCode.SUCCESS)
                            {
                                Debug.err(getClass().getName(),
                                        "Problem with updating the state!");
                                mRenderer.unlock();
                                return ret;
                            }

                            mAccumulator -= TIME_STEP;
                            if (mAccumulator > TIME_STEP)
                            {
                                Debug.inf(getClass().getName(),
                                        "Going to call update again!");
                            }
                        }

                        ret = currentState.render();
                        if (ret != RetCode.SUCCESS)
                        {
                            Debug.err(getClass().getName(),
                                    "Problem with rendering the state!");
                            mRenderer.unlock();
                            return ret;
                        }

                        if (mFpsVisible)
                        {
                            renderFps();
                        }
                    }

                    Fps.update();
                }
                finally
                {
                    // An exception occured, but try to unlock the canvas.
                    mRenderer.unlock();
                }
            }
            else
            {
                Debug.inf(getClass().getName(),
                        "Screen not ready. Don't render.");
            }
        }

        return ret;
    }

    /**
     * Changes current game state. It replaces all active states with the new
     * one.
     */
    public int changeState(GameState gameState)
    {
        Debug.inf(getClass().getName(), "changeState()");

        if (gameState == null)
        {
            Debug.err(getClass().getName(), "Bad parameter!");
            return RetCode.BAD_PARAM;
        }

        // Remove / terminate all the states in the stack.
        int ret = RetCode.SUCCESS;

        ret = removeGameStates();
        if (ret != RetCode.SUCCESS)
        {
            Debug.err(getClass().getName(),
                    "Problem with removing the obsolete game states!");
            return ret;
        }

        mGameStatesStack.push(gameState);
        ret = mGameStatesStack.peek().init();
        if (ret != RetCode.SUCCESS)
        {
            Debug.err(getClass().getName(),
                    "Problem with initializing the game state!");
            return ret;
        }

        return ret;
    }

    /**
     * Pauses the current state and put the new state on the top.
     */
    public int pushState(GameState gameState)
    {
        Debug.inf(getClass().getName(), "pushState()");

        if (gameState == null)
        {
            Debug.err(getClass().getName(), "Bad parameter!");
            return RetCode.BAD_PARAM;
        }

        int ret = RetCode.SUCCESS;

        if (mGameStatesStack.isEmpty() == false)
        {
            // Pause the current state.
            ret = mGameStatesStack.peek().pause();
            if (ret != RetCode.SUCCESS)
            {
                Debug.err(getClass().getName(),
                        "Problem with pausing the game state!");
                return ret;
            }
        }

        mGameStatesStack.push(gameState);
        ret = mGameStatesStack.peek().init();
        if (ret != RetCode.SUCCESS)
        {
            Debug.err(getClass().getName(),
                    "Problem with initializing the game state!");
            return ret;
        }

        return ret;
    }

    /**
     * Pops the current state and resumes the previous state.
     */
    public int popState()
    {
        Debug.inf(getClass().getName(), "popState()");

        int ret = RetCode.SUCCESS;

        if (mGameStatesStack.isEmpty() == false)
        {
            ret = mGameStatesStack.peek().term();
            if (ret != RetCode.SUCCESS)
            {
                Debug.err(getClass().getName(),
                        "Problem with terminating the game stae!");
                return ret;
            }

            mGameStatesStack.pop();
        }

        // Resume the next state (if exists).
        if (mGameStatesStack.isEmpty() == false)
        {
            ret = mGameStatesStack.peek().resume();
            if (ret != RetCode.SUCCESS)
            {
                Debug.err(getClass().getName(),
                        "Problem with resuming the game state!");
                return ret;
            }
        }

        return ret;
    }

    public boolean keyDown(int keyCode, KeyEvent msg)
    {
        synchronized (mLock)
        {
            if (mGameStatesStack.isEmpty() == false)
            {
                return mGameStatesStack.peek().onKeyDown(keyCode, msg);
            }
        }

        return false;
    }

    public boolean keyUp(int keyCode, KeyEvent msg)
    {
        synchronized (mLock)
        {
            if (mGameStatesStack.isEmpty() == false)
            {
                return mGameStatesStack.peek().onKeyUp(keyCode, msg);
            }
        }

        return false;
    }

    public boolean touchEvent(MotionEvent event)
    {
        synchronized (mLock)
        {
            if (mGameStatesStack.isEmpty() == false)
            {
                return mGameStatesStack.peek().onTouchEvent(event);
            }
            else
            {
                Debug.inf(getClass().getName(),
                        "Touch event, going to close app!");
                quit();
            }
        }

        return false;
    }

    public void quit()
    {
        // Core will be terminated from activity onDestroy method.
        Activity act = (Activity) mContext;
        act.finish();
    }

    public void focusChanged(boolean hasWindowFocus)
    {
        // TODO implement focusChanged.
    }

    public TimerManager getTimerManager()
    {
        return mTimerManager;
    }

    public SoundManager getSoundManager()
    {
        return mSoundManager;
    }

    public TextureManager getTextureManager()
    {
        return mTextureManager;
    }

    public SceneManager getSceneManager()
    {
        return mSceneManager;
    }

    public Renderer getRenderer()
    {
        return mRenderer;
    }

    public Context getContext()
    {
        return mContext;
    }

    public void screenCreated(SurfaceHolder surface)
    {
        synchronized (mLock)
        {
            // Notify the renderer that screen created.
            mSurface = surface;
            mRenderer.screenCreated(surface);

            //if (mFirstStart)
            {
                // Screen created so we may start the rendering thread.
                Debug.inf(getClass().getName(), "Starting the thread");
                int ret = Core.getInstance().start();
                if (ret != RetCode.SUCCESS)
                {
                    Debug.err(getClass().getName(), "Couldn't start the core!");
                }
                mScreenExists = true;
                mFirstStart = false;
            }
            //else
            if (mFirstStart)
            {
                Debug.inf(getClass().getName(), "Screen created!");

                GameState currentState = null;
                if (mGameStatesStack.isEmpty() == false)
                {
                    currentState = mGameStatesStack.peek();
                    currentState.onScreenCreated();
                }
            }
        }

        Debug.inf(getClass().getName(), "screenCreated");
    }

    public void screenDestroyed(SurfaceHolder surface)
    {
        synchronized (mLock)
        {
            mRenderer.screenDestroyed();

            GameState currentState = null;
            if (mGameStatesStack.isEmpty() == false)
            {
                currentState = mGameStatesStack.peek();
                currentState.onScreenDestroyed();
            }
            mScreenExists = false;
        }

        Debug.inf(getClass().getName(), "screenDestroyed");
    }

    public void screenChanged(SurfaceHolder surface, int width, int height)
    {
        synchronized (mLock)
        {
            mRenderer.screenChanged(width, height);

            GameState currentState = null;
            if (mGameStatesStack.isEmpty() == false)
            {
                currentState = mGameStatesStack.peek();
                currentState.onScreenChanged(width, height);
            }
            mScreenExists = true;
        }
        Debug.inf(getClass().getName(), "screenChanged, width: " + width
                + ", height: " + height);
    }

    public void showFps()
    {
        mFpsVisible = true;
    }

    public void hideFps()
    {
        mFpsVisible = false;
    }

    public void setFpsPosition(int x, int y)
    {
        mFpsPosition.x = x;
        mFpsPosition.y = y;
    }

    private void renderFps()
    {
        mRenderer.setColor(Color.WHITE);
        mRenderer.setTextSize(20);
        String secs = "FPS: " + Fps.get();
        mRenderer.drawText(secs, mFpsPosition.x, mFpsPosition.y);
    }

    private int removeGameStates()
    {
        int ret = RetCode.SUCCESS;
        GameState tmpState = null;

        Debug.inf(getClass().getName(), "Removing game states");

        while (mGameStatesStack.isEmpty() == false)
        {
            tmpState = mGameStatesStack.pop();
            ret = tmpState.term();
            if (ret != RetCode.SUCCESS)
            {
                Debug.err(getClass().getName(),
                        "Problem with terminating the game state!");
                return ret;
            }
        }

        return ret;
    }
}
